package org.lex.swing.widgets.console;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

import org.lex.swing.Swings;

/**
 * A Swing based console.
 */
public class TextPaneConsole extends AbstractSwingConsole implements IGUIConsole {
	private static final long serialVersionUID = 587657756471008857L;

	private int cmdStart = 0;
	private List<String> history = new Vector<String>();
	private String startedLine;
	private int histLine = 0;
	private JTextPane textPane;
	NameCompletion nameCompletion;
	final int SHOW_AMBIG_MAX = 10;
	// hack to prevent key repeat for some reason?
	private boolean gotUp = true;

	public static void main(String[] args) {
		TextPaneConsole con = new TextPaneConsole();
		con.addConsoleListener(new ConsoleListener() {
			public void execute(String command) {
				System.out.println("[" + command.trim() + "]");
			}
		});
		Swings.show(con);
	}

	public TextPaneConsole() {
		super(new BorderLayout());
		textPane = new JTextPane() {// new DefaultStyledDocument() ){
			public void cut() {
				if (textPane.getCaretPosition() < cmdStart) {
					super.copy();
				} else {
					super.cut();
				}
			}

			public void paste() {
				forceCaretMoveToEnd();
				super.paste();
			}
		};
		Font font = new Font("Monospaced", Font.PLAIN, 14);
		textPane.setText("");
		textPane.setFont(font);
		textPane.setMargin(new Insets(5, 5, 6, 6));
		textPane.setComponentPopupMenu(new TextPopup(textPane));
		textPane.addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent e) {
				processKeyTyped(e);
				gotUp = false;
			}

			public void keyTyped(KeyEvent e) {
				processKeyTyped(e);
			}

			public void keyReleased(KeyEvent e) {
				gotUp = true;
				processKeyTyped(e);
			}
		});
		this.add(new JScrollPane(textPane), BorderLayout.CENTER);
		requestFocus();
	}

	public void requestFocus() {
		super.requestFocus();
		textPane.requestFocus();
	}

	public void setFont(Font font) {
		super.setFont(font);
		if (textPane != null)
			textPane.setFont(font);
	}

	public void clear() {
	}

	public TextPaneConsole print(final Object o) {
		invokeAndWait(new Runnable() {
			public void run() {
				append(String.valueOf(o));
				resetCommandStart();
				textPane.setCaretPosition(cmdStart);
			}
		});
		return this;
	}

	public TextPaneConsole error(Object o) {
		print(o, Color.red);
		println();
		return this;
	}

	public TextPaneConsole println(Icon icon) {
		print(icon);
		println();
		textPane.repaint();
		return this;
	}

	public TextPaneConsole print(final Icon icon) {
		if (icon == null)
			return this;
		invokeAndWait(new Runnable() {
			public void run() {
				textPane.insertIcon(icon);
				resetCommandStart();
				textPane.setCaretPosition(cmdStart);
			}
		});
		return this;
	}

	public TextPaneConsole print(Object s, Font font) {
		print(s, font, null);
		return this;
	}

	public TextPaneConsole print(Object s, Color color) {
		return print(s, null, color);
	}

	public TextPaneConsole print(final Object o, final Font font, final Color color) {
		invokeAndWait(new Runnable() {
			public void run() {
				AttributeSet old = getStyle();
				setStyle(font, color);
				append(String.valueOf(o));
				resetCommandStart();
				textPane.setCaretPosition(cmdStart);
				setStyle(old, true);
			}
		});
		return this;
	}

	public void print(Object s, String fontFamilyName, int size, Color color) {
		print(s, fontFamilyName, size, color, false, false, false);
	}

	public void print(final Object o, final String fontFamilyName, final int size, final Color color, final boolean bold, final boolean italic,
			final boolean underline) {
		invokeAndWait(new Runnable() {
			public void run() {
				AttributeSet old = getStyle();
				setStyle(fontFamilyName, size, color, bold, italic, underline);
				append(String.valueOf(o));
				resetCommandStart();
				textPane.setCaretPosition(cmdStart);
				setStyle(old, true);
			}
		});
	}

	private synchronized void processKeyTyped(KeyEvent e) {
		switch (e.getKeyCode()) {
		case (KeyEvent.VK_ENTER):
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				if (gotUp) {
					enter();
					resetCommandStart();
					textPane.setCaretPosition(cmdStart);
				}
			}
			e.consume();
			textPane.repaint();
			break;
		case (KeyEvent.VK_UP):
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				historyUp();
			}
			e.consume();
			break;
		case (KeyEvent.VK_DOWN):
			if (e.getID() == KeyEvent.KEY_PRESSED) {
				historyDown();
			}
			e.consume();
			break;
		case (KeyEvent.VK_LEFT):
		case (KeyEvent.VK_BACK_SPACE):
		case (KeyEvent.VK_DELETE):
			if (textPane.getCaretPosition() <= cmdStart) {
				// This doesn't work for backspace.
				// See default case for workaround
				e.consume();
			}
			break;
		case (KeyEvent.VK_RIGHT):
			forceCaretMoveToStart();
			break;
		case (KeyEvent.VK_HOME):
			textPane.setCaretPosition(cmdStart);
			e.consume();
			break;
		case (KeyEvent.VK_U): // clear line
			if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) {
				replaceRange("", cmdStart, textLength());
				histLine = 0;
				e.consume();
			}
			break;
		case (KeyEvent.VK_ALT):
		case (KeyEvent.VK_CAPS_LOCK):
		case (KeyEvent.VK_CONTROL):
		case (KeyEvent.VK_META):
		case (KeyEvent.VK_SHIFT):
		case (KeyEvent.VK_PRINTSCREEN):
		case (KeyEvent.VK_SCROLL_LOCK):
		case (KeyEvent.VK_PAUSE):
		case (KeyEvent.VK_INSERT):
		case (KeyEvent.VK_F1):
		case (KeyEvent.VK_F2):
		case (KeyEvent.VK_F3):
		case (KeyEvent.VK_F4):
		case (KeyEvent.VK_F5):
		case (KeyEvent.VK_F6):
		case (KeyEvent.VK_F7):
		case (KeyEvent.VK_F8):
		case (KeyEvent.VK_F9):
		case (KeyEvent.VK_F10):
		case (KeyEvent.VK_F11):
		case (KeyEvent.VK_F12):
		case (KeyEvent.VK_ESCAPE):
			// only modifier pressed
			break;
		// Control-C
		case (KeyEvent.VK_C):
			if (textPane.getSelectedText() == null) {
				if (((e.getModifiers() & InputEvent.CTRL_MASK) > 0) && (e.getID() == KeyEvent.KEY_PRESSED)) {
					append("^C");
				}
				e.consume();
			}
			break;
		case (KeyEvent.VK_TAB):
			if (e.getID() == KeyEvent.KEY_RELEASED) {
				String part = textPane.getText().substring(cmdStart);
				doCommandCompletion(part);
			}
			e.consume();
			break;
		default:
			if ((e.getModifiers() & (InputEvent.CTRL_MASK | InputEvent.ALT_MASK | InputEvent.META_MASK)) == 0) {
				// plain character
				forceCaretMoveToEnd();
			}
			/*
			 * The getKeyCode function always returns VK_UNDEFINED for keyTyped
			 * events, so backspace is not fully consumed.
			 */
			if (e.paramString().indexOf("Backspace") != -1) {
				if (textPane.getCaretPosition() <= cmdStart) {
					e.consume();
					break;
				}
			}
			break;
		}
	}

	private void doCommandCompletion(String part) {
		if (nameCompletion == null)
			return;
		int i = part.length() - 1;
		// Character.isJavaIdentifierPart() How convenient for us!!
		while (i >= 0 && (Character.isJavaIdentifierPart(part.charAt(i)) || part.charAt(i) == '.'))
			i--;
		part = part.substring(i + 1);
		if (part.length() < 2) // reasonable completion length
			return;
		// System.out.println("completing part: "+part);
		// no completion
		String[] complete = nameCompletion.completeName(part);
		if (complete.length == 0) {
			java.awt.Toolkit.getDefaultToolkit().beep();
			return;
		}
		// Found one completion (possibly what we already have)
		if (complete.length == 1 && !complete.equals(part)) {
			String append = complete[0].substring(part.length());
			append(append);
			return;
		}
		// Found ambiguous, show (some of) them
		String line = textPane.getText();
		String command = line.substring(cmdStart);
		// Find prompt
		for (i = cmdStart; line.charAt(i) != '\n' && i > 0; i--)
			;
		String prompt = line.substring(i + 1, cmdStart);
		// Show ambiguous
		StringBuffer sb = new StringBuffer("\n");
		for (i = 0; i < complete.length && i < SHOW_AMBIG_MAX; i++)
			sb.append(complete[i] + "\n");
		if (i == SHOW_AMBIG_MAX)
			sb.append("...\n");
		print(sb, Color.gray);
		print(prompt); // print resets command start
		append(command); // append does not reset command start
	}

	private void resetCommandStart() {
		cmdStart = textLength();
	}

	private void append(String string) {
		int slen = textLength();
		textPane.select(slen, slen);
		textPane.replaceSelection(string);
	}

	private String replaceRange(Object s, int start, int end) {
		String st = s.toString();
		textPane.select(start, end);
		textPane.replaceSelection(st);
		// text.repaint();
		return st;
	}

	private void forceCaretMoveToEnd() {
		if (textPane.getCaretPosition() < cmdStart) {
			// move caret first!
			textPane.setCaretPosition(textLength());
		}
		textPane.repaint();
	}

	private void forceCaretMoveToStart() {
		if (textPane.getCaretPosition() < cmdStart) {
			// move caret first!
		}
		textPane.repaint();
	}

	private void enter() {
		String s = getCmd();
		if (s.length() == 0) // special hack for empty return!
			s = "\n";
		else {
			history.add(s);
			s = s + "\n";
		}
		append("\n");
		histLine = 0;
		acceptLine(s);
		textPane.repaint();
	}

	private String getCmd() {
		String s = "";
		try {
			s = textPane.getText(cmdStart, textLength() - cmdStart);
		} catch (BadLocationException e) {
			// should not happen
			System.out.println("Internal JConsole Error: " + e);
		}
		return s;
	}

	private void historyUp() {
		if (history.size() == 0)
			return;
		if (histLine == 0) // save current line
			startedLine = getCmd();
		if (histLine < history.size()) {
			histLine++;
			showHistoryLine();
		}
	}

	private void historyDown() {
		if (histLine == 0)
			return;
		histLine--;
		showHistoryLine();
	}

	private void showHistoryLine() {
		String showline;
		if (histLine == 0)
			showline = startedLine;
		else
			showline = (String) history.get(history.size() - histLine);
		replaceRange(showline, cmdStart, textLength());
		textPane.setCaretPosition(textLength());
		textPane.repaint();
	}

	String ZEROS = "000";

	private void acceptLine(String line) {
		// Patch to handle Unicode characters
		// Submitted by Daniel Leuck
		// StringBuffer buf = new StringBuffer();
		// int lineLength = line.length();
		// for( int i = 0; i < lineLength; i++ )
		// {
		// char c = line.charAt( i );
		// if( c > 127 )
		// {
		// String val = Integer.toString( c, 16 );
		// val = ZEROS.substring( 0, 4 - val.length() ) + val;
		// buf.append( "\\u" + val );
		// }
		// else
		// {
		// buf.append( c );
		// }
		// }
		// line = buf.toString();
		// End unicode patch
		this.fireConsoleExecute(line);
	}

	private AttributeSet setStyle(Font font, Color color) {
		if (font != null)
			return setStyle(font.getFamily(), font.getSize(), color, font.isBold(), font.isItalic(), StyleConstants.isUnderline(getStyle()));
		else
			return setStyle(null, -1, color);
	}

	private AttributeSet setStyle(String fontFamilyName, int size, Color color) {
		MutableAttributeSet attr = new SimpleAttributeSet();
		if (color != null)
			StyleConstants.setForeground(attr, color);
		if (fontFamilyName != null)
			StyleConstants.setFontFamily(attr, fontFamilyName);
		if (size != -1)
			StyleConstants.setFontSize(attr, size);
		setStyle(attr);
		return getStyle();
	}

	private AttributeSet setStyle(String fontFamilyName, int size, Color color, boolean bold, boolean italic, boolean underline) {
		MutableAttributeSet attr = new SimpleAttributeSet();
		if (color != null)
			StyleConstants.setForeground(attr, color);
		if (fontFamilyName != null)
			StyleConstants.setFontFamily(attr, fontFamilyName);
		if (size != -1)
			StyleConstants.setFontSize(attr, size);
		StyleConstants.setBold(attr, bold);
		StyleConstants.setItalic(attr, italic);
		StyleConstants.setUnderline(attr, underline);
		setStyle(attr);
		return getStyle();
	}

	private void setStyle(AttributeSet attributes) {
		setStyle(attributes, false);
	}

	private void setStyle(AttributeSet attributes, boolean overWrite) {
		textPane.setCharacterAttributes(attributes, overWrite);
	}

	private AttributeSet getStyle() {
		return textPane.getCharacterAttributes();
	}

	public String toString() {
		return "JConsole";
	}

	/**
	 * If not in the event thread run via SwingUtilities.invokeAndWait()
	 */
	private void invokeAndWait(Runnable run) {
		if (!SwingUtilities.isEventDispatchThread()) {
			try {
				SwingUtilities.invokeAndWait(run);
			} catch (Exception e) {
				// shouldn't happen
				e.printStackTrace();
			}
		} else {
			run.run();
		}
	}

	public void setNameCompletion(NameCompletion nc) {
		this.nameCompletion = nc;
	}

	public void setWaitFeedback(boolean on) {
		if (on)
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
		else
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
	}

	private int textLength() {
		return textPane.getDocument().getLength();
	}

	private static class TextPopup extends JPopupMenu implements ActionListener {
		private final static String CUT = "Cut";
		private final static String COPY = "Copy";
		private final static String PASTE = "Paste";
		private JTextComponent text = null;

		TextPopup(JTextComponent text) {
			this.text = text;
			this.addMenu(CUT);
			this.addMenu(COPY);
			this.addMenu(PASTE);
			UIManager.addPropertyChangeListener(new PropertyChangeListener() {
				public void propertyChange(PropertyChangeEvent e) {
					if (e.getPropertyName().equals("lookAndFeel")) {
						SwingUtilities.updateComponentTreeUI(TextPopup.this);
					}
				}
			});
		}

		public void actionPerformed(ActionEvent e) {
			String ac = e.getActionCommand();
			if (ac == CUT) {
				text.cut();
			} else if (ac == COPY) {
				text.copy();
			} else if (ac == PASTE) {
				text.paste();
			}
		}

		private void addMenu(String text) {
			JMenuItem mi = this.add(text);
			mi.setActionCommand(text);
			mi.addActionListener(this);
		}
	}
}
